<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <script>
    const template = `<div id="a">
        基础类型:{{a}}, 数组: {{arr}}, 对象: {{obj}} 
        <p>
          <div><span>fdasfa</span></div>  
        </p>
        </div>`
    function init(el) {
      const p = new Parse(el)
      console.log(p.parse());
    }
    // 以下为源码的正则  对正则表达式不清楚的同学可以参考小编之前写的文章(前端进阶高薪必看 - 正则篇);
    const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //匹配标签名 形如 abc-123
    const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //匹配特殊标签 形如 abc:234 前面的abc:可有可无
    const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配标签开始 形如 <abc-123 捕获里面的标签名
    const startTagClose = /^\s*(\/?)>/; // 匹配标签结束  >
    const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾 如 </abc-123> 捕获里面的标签名
    const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性  形如 id="app"
    // 标识元素和文本type
    const ELEMENT_TYPE = 1;
    const TEXT_TYPE = 3;

    class Parse {
      constructor(html) {
        this.html = html
        this.root = null
        this.elementParent = null
        this.stack = []
      }

      parse() {
        while (this.html) {
          const startTagIndex = this.html.indexOf('<')
          // 处理开始标签
          if (startTagIndex === 0) {
            const startTag = this.parseStartTag()
            if (startTag) {
              this.handleStartTag(startTag)
              continue
            }
            const endTagMatch = this.html.match(endTag)
            if (endTagMatch) {
              this.advance(endTagMatch[0].length)
              this.handleEndTag()
              continue
            }

          }
          // 处理文件
          let text
          if (startTagIndex > 0) {
            text = this.html.substring(0, startTagIndex)
          }
          if (text) {
            this.advance(text.length)
            this.handleChars(text);
          }
        }
        return this.root
      }
      handleStartTag({ tagName, attrs }) {
        const element = this.createASTElement(tagName, attrs)
        if (!this.root) {
          this.root = element
        }
        this.elementParent = element
        this.stack.push(element)
      }

      parseStartTag() {
        // 读取标签名称
        const start = this.html.match(startTagOpen)
        if (start) {
          const match = {
            tagName: start[1],
            attrs: []
          }
          this.advance(start[1].length)
          let end, attr
          while (
            !(end = this.html.match(startTagClose)) &&
            (attr = this.html.match(attribute))
          ) {
            this.advance(attr[0].length);
            attr = {
              name: attr[1],
              value: attr[3] || attr[4] || attr[5], //这里是因为正则捕获支持双引号 单引号 和无引号的属性值
            };
            match.attrs.push(attr)
          }
          if (end) {
            this.advance(1)
            return match
          }
        }

      }
      // 生成ast结构
      createASTElement(tagName, attrs) {
        return {
          tag: tagName,
          type: ELEMENT_TYPE,
          children: [],
          attrs,
          parent: null,
        };
      }

      handleChars(text) {
        text = text.replace(/\s/g, '')
        if (text) {
          this.elementParent.children.push({
            type: TEXT_TYPE,
            text
          })
        }
      }

      handleEndTag() {
        const element = this.stack.pop()
        this.elementParent = this.stack[this.stack.length - 1]
        if (this.elementParent) {
          element.parent = this.elementParent
          this.elementParent.children.push(element)
        }
      }

      advance(length) {
        this.html = this.html.substring(length)
      }
    }

    init(template)
  </script>
</body>

</html>